@ragrabbit/mcp

by madarco
Verified
import { authOrLogin } from "@repo/auth"; import db from "@repo/db"; import { and, eq } from "@repo/db/drizzle"; import { Indexed, IndexedContent, indexedTable, llamaindexEmbedding } from "@repo/db/schema"; import { ArrowLeft, FileText, ListTree, Search, FileIcon, Calendar, Hash, Type, FileQuestion, } from "@repo/design/base/icons"; import { Button } from "@repo/design/shadcn/button"; import { Card, CardContent } from "@repo/design/shadcn/card"; import { Separator } from "@repo/design/shadcn/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/design/shadcn/tabs"; import Link from "next/link"; import { notFound } from "next/navigation"; import { ReEmbeddingsButton } from "../components/buttons"; import { ReScrapeButton } from "../components/buttons"; import { RagMetadata } from "@repo/rag/indexing/metadata.type"; import EditButton from "../components/edit-button"; function formatBytes(bytes: number) { if (!bytes) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } function countWords(str: string) { return str.trim().split(/\s+/).length; } export default async function IndexContentPage({ params }: { params: { id: string } }) { const session = await authOrLogin(); const { id } = await params; const indexId = parseInt(id); if (isNaN(indexId)) { notFound(); } const indexed: Indexed & { indexedContent: IndexedContent } = (await db.query.indexedTable.findFirst({ where: and(eq(indexedTable.id, indexId), eq(indexedTable.organizationId, session.user.organizationId)), with: { indexedContent: true, }, })) as any; if (!indexed) { notFound(); } // Fetch embeddings for this content const embeddings = await db.select().from(llamaindexEmbedding).where(eq(llamaindexEmbedding.contentId, indexId)); const indexedContent = indexed.indexedContent; const metadata = embeddings[0]?.metadata as RagMetadata; const wordCount = indexedContent ? countWords(indexedContent.content) : 0; const contentSize = indexedContent ? new TextEncoder().encode(indexedContent.content).length : 0; // Group entities by type const groupedEntities = metadata?.entities?.reduce((acc: Record<string, string[]>, entity) => { if (!acc[entity.type]) { acc[entity.type] = []; } acc[entity.type].push(entity.name); return acc; }, {}) || {}; const totalTokens = metadata?.tokens || 0; return ( <> <div className="flex items-center justify-between space-y-2 mb-8"> <div className="space-y-1"> <div className="flex items-center gap-2"> <Button variant="outline" size="icon" asChild className="h-8 w-8"> <Link href="/dashboard/indexing"> <ArrowLeft className="h-4 w-4" /> </Link> </Button> <h2 className="text-2xl font-bold tracking-tight">{indexed.title || "Untitled Content"}</h2> </div> <p className="text-muted-foreground">View and analyze the indexed content details</p> </div> <div className="flex items-center space-x-2"> <ReScrapeButton indexIds={[indexId]} refresh={true} /> <ReEmbeddingsButton indexIds={[indexId]} refresh={true} /> <EditButton index={indexed} /> </div> </div> <div className="flex flex-col gap-6"> <Card> <CardContent className="pt-6"> <div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <FileIcon className="h-4 w-4" /> Title </div> <div className="font-medium truncate">{indexed.title}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Type className="h-4 w-4" /> Type </div> <div className="font-medium">Web Page</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Hash className="h-4 w-4" /> Size </div> <div className="font-medium">{formatBytes(contentSize)}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <FileQuestion className="h-4 w-4" /> Status </div> <div className="font-medium capitalize">{indexed.status.toLowerCase()}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Calendar className="h-4 w-4" /> Indexed Date </div> <div className="font-medium">{indexed.indexedAt?.toLocaleDateString() || "Pending"}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Hash className="h-4 w-4" /> Word Count </div> <div className="font-medium">{wordCount.toLocaleString()}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Hash className="h-4 w-4" /> Tokens </div> <div className="font-medium">{totalTokens.toLocaleString()}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <ListTree className="h-4 w-4" /> Chunks </div> <div className="font-medium">{embeddings.length}</div> </div> </div> </CardContent> </Card> <Tabs defaultValue="analysis" className="w-full"> <TabsList> <TabsTrigger value="analysis" className="flex items-center gap-2"> <Search className="h-4 w-4" /> Analysis </TabsTrigger> <TabsTrigger value="original" className="flex items-center gap-2"> <FileText className="h-4 w-4" /> Original Text </TabsTrigger> <TabsTrigger value="chunks" className="flex items-center gap-2"> <ListTree className="h-4 w-4" /> Chunks </TabsTrigger> </TabsList> <TabsContent value="analysis"> <Card> <CardContent className="pt-6"> <div className="space-y-6"> <div> <div className="flex items-center gap-2 mb-2"> <FileText className="h-4 w-4" /> <h3 className="text-lg font-semibold">Title</h3> </div> <p className="text-muted-foreground">{metadata?.pageTitle || "No title"}</p> </div> <div> <div className="flex items-center gap-2 mb-2"> <FileText className="h-4 w-4" /> <h3 className="text-lg font-semibold">Summary</h3> </div> <p className="text-muted-foreground">{metadata?.pageDescription || "No description"}</p> </div> <div> <div className="flex items-center gap-2 mb-2"> <FileQuestion className="h-4 w-4" /> <h3 className="text-lg font-semibold">Questions</h3> </div> <div className="flex flex-wrap gap-2"> {metadata?.questions?.map((question, i) => ( <span key={i} className="rounded-full bg-primary/10 text-primary px-3 py-1 text-sm"> {question} </span> )) || "No questions"} </div> </div> <div> <div className="flex items-center gap-2 mb-2"> <Hash className="h-4 w-4" /> <h3 className="text-lg font-semibold">Keywords</h3> </div> <div className="flex flex-wrap gap-2"> {metadata?.keywords?.map((keyword, i) => ( <span key={i} className="rounded-full bg-background border px-3 py-1 text-sm"> {keyword} </span> )) || "No keywords"} </div> </div> <div> <div className="flex items-center gap-2 mb-2"> <Type className="h-4 w-4" /> <h3 className="text-lg font-semibold">Entities</h3> </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {Object.entries(groupedEntities).map(([type, entities]) => ( <div key={type} className="space-y-2"> <h4 className="text-sm font-medium text-muted-foreground capitalize">{type}</h4> <div className="flex flex-wrap gap-2"> {entities.map((name, i) => ( <span key={i} className="rounded-full bg-background border px-3 py-1 text-sm"> {name} </span> ))} </div> </div> ))} {Object.keys(groupedEntities).length === 0 && <p>No entities</p>} </div> </div> </div> </CardContent> </Card> </TabsContent> <TabsContent value="original"> <Card> <CardContent className="pt-6 bg-muted/50"> <pre className="whitespace-pre-wrap text-sm rounded-lg">{indexedContent?.content || "No content"}</pre> </CardContent> </Card> </TabsContent> <TabsContent value="chunks"> <Card> <CardContent className="pt-6 space-y-6"> <div className="flex items-center justify-between"> <div className="space-y-1"> <h3 className="text-lg font-semibold">Content Chunks</h3> <p className="text-sm text-muted-foreground"> {embeddings.length} chunks, {totalTokens.toLocaleString()} tokens total </p> </div> </div> <div className="space-y-4"> {embeddings.map((embedding, i) => ( <div key={i} className="border rounded-lg"> <div className="border-b bg-muted/50 px-4 py-2 flex items-center justify-between"> <div className="font-medium">Chunk {i + 1}</div> <div className="text-sm text-muted-foreground"> {embedding.document.length.toLocaleString()} characters </div> </div> <div className="p-4"> <pre className="whitespace-pre-wrap text-sm max-h-[200px] overflow-y-auto"> {embedding.document} </pre> </div> </div> ))} {embeddings.length === 0 && ( <div className="text-center py-4 text-muted-foreground">No chunks available</div> )} </div> </CardContent> </Card> </TabsContent> </Tabs> </div> </> ); }